/*
 * Albareth - an action roleplaying game.
 * Copyright (C) 2008 Jens Anuth
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *   02110-1301, USA.
 * 
 * $Id: TownMapGenerator.java 43 2008-05-13 16:52:12Z jens464 $
*/
package albareth.generator;

import albareth.core.entity.Ground;
import albareth.core.entity.GroundType;
import albareth.core.entity.Item;
import albareth.core.entity.ItemFactory;
import albareth.core.entity.Mobile;
import albareth.core.entity.MobileFactory;
import albareth.core.entity.item.Door;
import albareth.core.entity.template.MobileTemplate;
import albareth.frontend.TileCollection;
import albareth.util.Location;
import frosch.util.Bresenham;
import frosch.util.Coordinates;
import frosch.util.FToolkit;
import frosch.util.Log;
import frosch.util.Range;
import frosch.util.Range2D;
import frosch.util.Rectangle;
import frosch.util.WeightedSet;


public class TownMapGenerator extends FractalMapGenerator
{
    
    private int _waterLevel;
    
    private Range _houseSize = new Range(4, 8);
    private Range2D _buildingRange;
    private int _townBuildingRecursionDepth = 8;
    private Location _playerStartingLocation = null;
    private Location _dungeonEntranceLocation = null;
    
    private int _countPaddocks = 0;
    private int _countHouses = 0;
    
    private static WeightedSet _setBushes = null;
    private static WeightedSet _setGrass = null;
    private static WeightedSet _setForest = null;
    private static WeightedSet _setHouseFloors = null;
    private static WeightedSet _setHouseWalls = null;
    private static WeightedSet _setHouseWindows = null;
    private static WeightedSet _setDungeonEntranceFloors = null;
    private static WeightedSet _setDungeonEntranceWalls = null;
    private static WeightedSet _setDungeonEntranceWindows = null;
    private static WeightedSet _setPaddockMuddy = null;
    private static WeightedSet _setPaddockGrassy = null;

    /* initialize ground tile sets */
    static
    {
        _setBushes = new WeightedSet();
        _setBushes.
                add(Ground.BUSHES_L, 1).
                add(Ground.BUSHES_M, 1).
                add(Ground.BUSHES_S, 2);

        _setGrass = new WeightedSet();
        _setGrass.
                add(Ground.GRASS_1, 2).
                add(Ground.GRASS_2, 3).
                add(Ground.GRASS_3, 3).
                add(Ground.BUSHES_S, 1);

        _setForest = new WeightedSet();
        _setForest.
                add(Ground.FOREST_M, 2).
                add(Ground.FOREST_S, 1);
       
        _setHouseFloors = new WeightedSet();
        _setHouseFloors.
                add(Ground.HOUSE_FLOOR_1, 1).
                add(Ground.HOUSE_FLOOR_2, 3).
                add(Ground.HOUSE_FLOOR_3, 3);
        
        _setHouseWalls = new WeightedSet();
        _setHouseWalls.
                add(Ground.HOUSE_WALL_1, 2).
                add(Ground.HOUSE_WALL_2, 4).
                add(Ground.HOUSE_WALL_3, 5);

        _setHouseWindows = new WeightedSet();
        _setHouseWindows.
                add(Ground.HOUSE_WINDOW_1, 1).
                add(Ground.HOUSE_WINDOW_2, 1).
                add(Ground.HOUSE_WINDOW_3, 1);

        _setDungeonEntranceFloors = new WeightedSet();
        _setDungeonEntranceFloors.
                add(Ground.FLOOR_DARK1, 1).
                add(Ground.FLOOR_DARK2, 5).
                add(Ground.FLOOR_DARK3, 5).
                add(Ground.FLOOR_LIGHT1, 3);

        _setDungeonEntranceWalls = new WeightedSet();
        _setDungeonEntranceWalls.
                add(Ground.FLOOR_DARK2, 1).
                add(Ground.FLOOR_DARK3, 2).
                add(Ground.FLOOR_LIGHT1, 2).
                add(Ground.WALL1, 3);

        _setDungeonEntranceWindows = new WeightedSet();
        _setDungeonEntranceWindows.
                add(Ground.FLOOR_DARK2, 1).
                add(Ground.FLOOR_DARK3, 2).
                add(Ground.FLOOR_LIGHT1, 2).
                add(Ground.DUNGEON_WINDOW1, 3);

        _setPaddockGrassy = new WeightedSet();
        _setPaddockGrassy.
                add(Ground.DIRT_LIGHT_1, 1).
                add(Ground.DIRT_LIGHT_2, 2).
                add(Ground.DIRT_LIGHT_3, 3).
                add(Ground.DIRT_GRASS_1, 2).
                add(Ground.DIRT_GRASS_2, 3);

        _setPaddockMuddy = new WeightedSet();
        _setPaddockMuddy.
                add(Ground.DIRT_1, 1).
                add(Ground.DIRT_2, 2).
                add(Ground.DIRT_3, 3).
                add(Ground.DIRT_MUD_1, 2).
                add(Ground.DIRT_MUD_2, 3);

    }
    
    class Street
    {
        int fromX;
        int fromY;
        int toX;
        int toY;
        int length;
        
        Street(int fromX, int fromY, int toX, int toY) {
            this.fromX = fromX;
            this.fromY = fromY;
            this.toX = toX;
            this.toY = toY;
        }
        
        boolean isHorizontal() {
            return fromY == toY;
        }
    }
    
    //XXX: use class Room
    class House extends Rectangle
    {
        Location doorLocation;

        House(int x, int y, int width, int height)
        {
            super(x, y, width, height);
            
        }
        
        public Location getRandomLocation()
        {
            return new Location(
                    FToolkit.rand(x, x + w),
                    FToolkit.rand(y, y + h),
                    map
            );
        }
    }
    
    public TownMapGenerator(int waterLevel)
    {
        super(8, 25, true);
        _waterLevel = waterLevel;
    }

    protected void transformToGameMap(int[][] fractalMap)
    {

        int h;
        //transform heights into tiles:
        for (int i = 0; i < map.getWidth(); i++)
        {
            for (int j = 0; j < map.getHeight(); j++)
            {
                h = fractalMap[i][j];
                //System.err.println(h);
                
                if      (h < -2)  map.setGround(Ground.MOUNTAINS_L, i, j);
                else if (h < 5)   map.setGround(Ground.MOUNTAINS_M, i, j);
                else if (h < 13)  map.setGround(Ground.MOUNTAINS_S, i, j);
                else if (h < 14)  map.setGround(Ground.FOREST_L, i, j);
                else if (h < 16)  map.setGround((Ground)_setForest.randomElement(), i, j);
                else if (h < 17)  map.setGround((Ground)_setBushes.randomElement(), i, j);
                else if (h < 28)  map.setGround((Ground)_setGrass.randomElement(), i, j);
                //else if (h < 23)  _gameMap.setGround(Ground.WATER_M, i, j);
                else if (h < 30)  map.setGround(Ground.WATER_M, i, j);
                else  map.setGround(Ground.WATER_L, i, j);
            }
        }
        finetuneLandscape();
        generateVillage();
    }

    /**
     * Delete all water-tiles with 4 or more adjacent land tiles.
     */
    protected void finetuneLandscape()
    {
        for (int x = 0; x < map.getWidth(); x++)
        {
            for (int y = 0; y < map.getHeight(); y++)
            {
                if (map.getGroundType(x, y) == GroundType.WATER)
                {
                    int adjacentLand = 0;

                    if (y > 0 && map.getGroundType(x, y - 1) != GroundType.WATER) {
                        adjacentLand++;
                    }

                    if (y < map.getHeight() - 1 &&
                        map.getGroundType(x, y + 1) != GroundType.WATER)
                    {
                        adjacentLand++;
                    }

                    if (x > 0 && map.getGroundType(x - 1, y) != GroundType.WATER) {
                        adjacentLand++;
                    }

                    if (x < map.getWidth() - 1 &&
                        map.getGroundType(x + 1, y) != GroundType.WATER)
                    {
                        adjacentLand++;
                    }

                    if (adjacentLand > 2) map.setGround((Ground)_setGrass.randomElement(), x, y);
                }
            }
        }
    }
    
    
    private void generateVillage()
    {
        _buildingRange = new Range2D(
                map.getWidth()/4, map.getHeight()/4,
                map.getWidth()/4*3, map.getHeight()/4*3
        );
        //first build a main-street and an attached house
        //it is random, but either horizontal or vertical.
        //the methods then recursively builds all other streets and houses
        Street street = buildHouseAndStreet(_buildingRange, 0);
        
        if (street == null) { //village-building failed for some reason?
            Log.warn("Village building attempt failed. Trying again.");
            generateVillage(); //XXX: better rebuild the whole map
        }
        _playerStartingLocation = new Location(
                street.toX, street.toY, map
        );
    }
    
    protected Location getEntranceLocation()
    {
        return _playerStartingLocation;
    }

    protected Location getExitLocation()
    {
        return _dungeonEntranceLocation;
    }

    
    /**
     * 
     * @return The street built.
     */
    private Street buildHouseAndStreet(Range2D streetRange, int recursionDepth)
    {
        if (recursionDepth > _townBuildingRecursionDepth) {
            return null;
        }
        
        //repeat 10 times. find house with longest street
        House bestHouseSoFar = null;
        Street bestConnectingStreetSoFar = null;
        int longestConnectingStreetSoFar = -1;
        for (int i = 0; i < 10; i++)
        {
            Street baseStreet = getConcreteStreet(streetRange);

            //determine house placement rectangles left and right of street:
            Range2D housePlaceAreaLeft;
            Range2D housePlaceAreaRight;
            if (baseStreet.isHorizontal())
            {
                housePlaceAreaLeft = new Range2D(
                        baseStreet.fromX, _buildingRange.getMinY(),
                        baseStreet.toX, baseStreet.toY-1
                );
                housePlaceAreaRight = new Range2D(
                        baseStreet.fromX, baseStreet.fromY+1,
                        baseStreet.toX, _buildingRange.getMaxY()
                );
            } else {
                housePlaceAreaLeft = new Range2D(
                        _buildingRange.getMinX(), baseStreet.fromY,
                        baseStreet.toX-1, baseStreet.toY
                );
                housePlaceAreaRight = new Range2D(
                        baseStreet.fromX+1, baseStreet.fromY,
                        _buildingRange.getMaxX(), baseStreet.toY
                );

            }

            House house;
            //now try to place a house. try one time left and 
            //one time right of street
            boolean firstTryLeft = FToolkit.flipCoin();
            house = findValidHouse(
                    firstTryLeft ? housePlaceAreaLeft : housePlaceAreaRight
            );
            if (house == null) {
                house = findValidHouse(
                        !firstTryLeft ? housePlaceAreaLeft : housePlaceAreaRight
                );
            }
            
            if (house == null) {
                continue; //not able to build a house here. next attempt
            }

            //try build connecting road from the house to baseStreet:
            Street connectingStreet =
                findConnectingStreet(house, baseStreet);
                
            if (connectingStreet == null) {
                    continue; //not able to connect house with street to
                              //baseStreet. next attempt
            }
            
            if (connectingStreet.length > longestConnectingStreetSoFar) {
                bestHouseSoFar = house;
                bestConnectingStreetSoFar = connectingStreet;
                longestConnectingStreetSoFar = connectingStreet.length;
            }
        }
        
        //now draw the best one of them:
        if (longestConnectingStreetSoFar > -1) {
            //draw into map
            drawEstate(bestHouseSoFar);
            //drawHouse(bestHouseSoFar);
            drawStreet(bestConnectingStreetSoFar);
            
            Range2D newStreetRange = new Range2D(
                    bestConnectingStreetSoFar.fromX,
                    bestConnectingStreetSoFar.fromY,
                    bestConnectingStreetSoFar.toX,
                    bestConnectingStreetSoFar.toY
            );
            buildHouseAndStreet(newStreetRange, recursionDepth+1);
            buildHouseAndStreet(newStreetRange, recursionDepth+1);
        }
        
        return bestConnectingStreetSoFar;
    }


    private Street getConcreteStreet(Range2D streetRange)
    {
        if (streetRange.getRangeX().getBreadth() == 0 ||
            streetRange.getRangeY().getBreadth() == 0) //horiz or vertical?
        {
            return new Street(
                    streetRange.getRangeX().getMin(),
                    streetRange.getRangeY().getMin(),
                    streetRange.getRangeX().getMax(),
                    streetRange.getRangeY().getMax()
            );
        } else //is neither. then either...
        {
            if (FToolkit.flipCoin()) //...we roll a horizontal street
            {
                int y = streetRange.getRangeY().getRandomValue();
                return new Street(
                        streetRange.getRangeX().getMin(),
                        y,
                        streetRange.getRangeX().getMax(),
                        y
                );
            } else { //or we roll a vertical street
                int x = streetRange.getRangeX().getRandomValue();
                return new Street(
                        x,
                        streetRange.getRangeY().getMin(),
                        x,
                        streetRange.getRangeY().getMax()
                );

            }
        }
    }

    
    
    private House findValidHouse(Range2D range2d)
    {
        if (range2d.getWidth() < _houseSize.getMin() ||
            range2d.getHeight() < _houseSize.getMin())
        {
            return null;
        }
        
        Range boundedHouseWidth = new Range(
                _houseSize.getMin(),
                range2d.getWidth() < _houseSize.getMax() ?
                        range2d.getWidth() : _houseSize.getMax()
        );
        Range boundedHouseHeight = new Range(
                _houseSize.getMin(),
                range2d.getHeight() < _houseSize.getMax() ?
                        range2d.getHeight() : _houseSize.getMax()
        );

        for (int tries = 0; tries < 10; tries++)
        {
            int width = boundedHouseWidth.getRandomValue();
            int height = boundedHouseHeight.getRandomValue();
            //System.err.println("w:" + width + " h:" + height);
            //System.err.println("rangex:" + range2d.getRangeX() + " rangey:" + range2d.getRangeY());
            int x = FToolkit.rand(range2d.getMinX(), range2d.getMaxX()+1 - width);
            int y = FToolkit.rand(range2d.getMinY(), range2d.getMaxY()+1 - height);
            
            if (isRectangleValidForHouse(x, y, width, height)) {
                return new House(x, y, width, height);
            }
        }
        
        return null;
    }

    
    private boolean isRectangleValidForHouse(int x, int y, int width, int height)
    {
        for (int i = -1; i <= width; i++)
        {
            for (int j = -1; j <= height; j++)
            {
                if (map.getGroundType(x+i, y+j) != GroundType.LEA &&
                    map.getGroundType(x+i, y+j) != GroundType.FOREST)
                {
                    return false;
                }
            }
        }
        return true;
    }

    private Street findConnectingStreet(House house, Street baseStreet)
    {
        int startXMin = house.x + 1;
        int startXMax = house.x + house.w - 2;
        int startYMin = house.y + 1;
        int startYMax = house.y + house.h - 2;

        for (int tries = 0; tries < 5; tries++)
        {
            //4 cases...
            Street testStreet;
            Coordinates doorLocation;
            if (baseStreet.isHorizontal())
            {
                int bresenX = FToolkit.rand(startXMin, startXMax);
                if (house.y < baseStreet.fromY) { //house above street
                    testStreet = new Street(
                            bresenX,
                            house.getBottomY() + 1,
                            bresenX,
                            baseStreet.fromY - 1
                    );
                    doorLocation = new Coordinates(
                            testStreet.fromX,
                            house.getBottomY()
                    );
                } else { //below street
                    testStreet = new Street(
                            bresenX,
                            house.getY() - 1,
                            bresenX,
                            baseStreet.fromY + 1
                    );
                    doorLocation = new Coordinates(
                            testStreet.fromX,
                            house.getY()
                    );

                }
            } else {
                int bresenY = FToolkit.rand(startYMin, startYMax);
                if (house.x < baseStreet.fromX) { //house left of street
                    testStreet = new Street(
                            house.getRightX() + 1,
                            bresenY,
                            baseStreet.fromX - 1,
                            bresenY
                    );
                    doorLocation = new Coordinates(
                            house.getRightX(),
                            testStreet.fromY
                    );


                } else { //right of street
                    testStreet = new Street(
                            house.getX() - 1,
                            bresenY,
                            baseStreet.fromX + 1,
                            bresenY
                    );
                    doorLocation = new Coordinates(
                            house.getX(),
                            testStreet.fromY
                    );

                }
            }

            //iterate the street, looking for blocked ground
            Bresenham bresen = new Bresenham(
                    testStreet.fromX,
                    testStreet.fromY,
                    testStreet.toX,
                    testStreet.toY
            );
            boolean blocked = false;
            boolean hasNext = true;
            while (hasNext)
            {
                hasNext = bresen.hasNext();
                if (map.getGroundType(bresen.x, bresen.y) != GroundType.LEA &&
                        map.getGroundType(bresen.x, bresen.y) != GroundType.FOREST)
                {
                    blocked = true;
                }
                bresen.iterate();
            };
            
            if (blocked == false) { //we found a uninterrupted connecting street:
                testStreet.length =
                    Math.abs(testStreet.fromX - testStreet.toX + 1) +
                    Math.abs(testStreet.fromY - testStreet.toY + 1);
                house.doorLocation = new Location(
                        doorLocation.x, doorLocation.y, map
                );
                return testStreet;
            }
        }

        return null;
    }
    

    /**
     * Decides randomly what to draw on the given house rectangle.
     */
    private void drawEstate(House house)
    {
        if (_dungeonEntranceLocation == null)
        {
            drawDungeonEntrance(house);
            int entranceX = FToolkit.rand(
                    house.getX() + 1, house.getRightX() - 1
            );
            int entranceY = FToolkit.rand(
                    house.getY() + 1, house.getBottomY() - 1
            );
            
            _dungeonEntranceLocation = new Location(
                    entranceX, entranceY, map
            );

        } else {
            if (FToolkit.rand(4) == 0) {
                drawPaddock(house);
            } else {
                drawHouse(house);
            }
        }
    }
    
    
    private void drawPaddock(House house)
    {
        //draw floor
        MapGeneratorTools.fillMapRect(
                map, house.x, house.y,
                house.w, house.h,
                FToolkit.flipCoin() ? _setPaddockGrassy : _setPaddockMuddy
        );
        
        MobileFactory factory = MobileFactory.getInstance();
        MobileTemplate tpl = factory.getRandomTemplate(MobileFactory.SET_FARM_ANIMALS);
        int numAnimals = FToolkit.rand(2, 5);
        for (int i = 0; i < numAnimals; i++)
        {
            Mobile animal = tpl.create();
            animal.settleTo(house.getRandomLocation(), 1);
        }
        
    }

    private void drawDungeonEntrance(House house)
    {
        drawHouse(
                house, _setDungeonEntranceFloors,
                _setDungeonEntranceWalls, _setDungeonEntranceWindows);
    }
    
    
    private void drawHouse(House house)
    {
        drawHouse(house, _setHouseFloors, _setHouseWalls, _setHouseWindows);
        //place door:
        if (house.doorLocation != null)
        {
            Item door = new Door(
                    "Door",
                    TileCollection.HOUSE_DOOR_CLOSED,
                    TileCollection.HOUSE_DOOR_OPEN,
                    false
            );
            door.moveTo(house.doorLocation);
        }

    }

    
    private void drawHouse(House house,
                           WeightedSet floorSet,
                           WeightedSet wallSet, WeightedSet windowSet)
    {
        //draw floor
        MapGeneratorTools.fillMapRect(
                map, house.x, house.y,
                house.w, house.h, floorSet
        );

        //draw walls and windows
        MapGeneratorTools.drawMapRect(
                map, house.x, house.y,
                house.w, house.h, WeightedSet.union(windowSet, wallSet)
        );
        
        //but no windows on the corners:
        map.setGround((Ground)wallSet.randomElement(),
                           house.x, house.y);
        map.setGround((Ground)wallSet.randomElement(),
                           house.getRightX(), house.y);
        map.setGround((Ground)wallSet.randomElement(),
                           house.x, house.getBottomY());
        map.setGround((Ground)wallSet.randomElement(),
                           house.getRightX(), house.getBottomY());
        
        if (house.doorLocation != null) {
            map.setGround(
                    (Ground)floorSet.randomElement(),
                    house.doorLocation.x, house.doorLocation.y
            );
        }
        
    }


    private void drawStreet(Street street)
    {
        Bresenham bresen = new Bresenham(
                street.fromX, street.fromY,
                street.toX, street.toY
        );

        boolean hasNext = true;
        while (hasNext)
        {
            hasNext = bresen.hasNext();
            map.setGround(Ground.FLOOR_LIGHT3, bresen.getX(), bresen.getY());
            bresen.iterate();
        }
    }

    


}


